package be.rivendale.geometry;

import be.rivendale.mathematics.Point;
import be.rivendale.mathematics.Triangle;
import be.rivendale.mathematics.Triple;
import org.apache.commons.lang.Validate;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Represents a triangle model or mesh.
 * The triangles of this mesh define a higher level geometric object that constist solely of triangles.
 * This is a typical polygon based representation as is also used in most modern hardware and rendering engines.?
 */
public class TriangleModel {
	/**
	 * The list of triangles that define this model.
	 */
    private List<Triangle> triangles;
    private AxisAlignedBoundingBox axisAlignedBoundingBox;

    /**
	 * Loads a model from a predefined list of triangles.
	 * <p>Usually this method is not very practical, since a model should be loaded from some kind of persistent
	 * format, but occasionally it comes in quite handy to be able to define a couple of triangles in-line, and
	 * treat it as a model.</p>
	 * @param triangles The triangles to set to this model.
	 */
	public TriangleModel(List<Triangle> triangles) {
		Validate.notNull(triangles, "Triangle list is required");
		this.triangles = new ArrayList<Triangle>(triangles);
        this.axisAlignedBoundingBox = calculateAxisAlignedBoundingBox();
        System.out.format("Created triangle model with %d triangles\n", triangles.size());
	}

    /**
	 * Retieves the list of triangles in this triangle model.
	 * @return The list of triangles in an unmodifiable collection.
	 */
    public List<Triangle> getTriangles() {
        return Collections.unmodifiableList(triangles);
    }

	/**
	 * Retrieves the closest possible {@link be.rivendale.geometry.AxisAlignedBoundingBox} around the entire model.
	 * @return The axis aligned bounding box around this model.
	 */
	public AxisAlignedBoundingBox getAxisAlignedBoundingBox() {
        return this.axisAlignedBoundingBox;
	}

    private AxisAlignedBoundingBox calculateAxisAlignedBoundingBox() {
        double[] currentMinimumBound = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE};
        double[] currentMaximumBound = {Double.MIN_VALUE, Double.MIN_VALUE, Double.MIN_VALUE};
        for(Triangle triangle : triangles) {
            AxisAlignedBoundingBox triangleBoundingBox = triangle.getAxisAlignedBoundingBox();
            retainMinimumCoordinates(triangleBoundingBox.getMinimumBound(), currentMinimumBound);
            retainMaximumCoordinates(triangleBoundingBox.getMaximumBound(), currentMaximumBound);
        }
        return new AxisAlignedBoundingBox(new Triple(currentMinimumBound), new Triple(currentMaximumBound));
    }

    /**
	 * Checks if the X, Y and Z values in the specified point are less then the ones found in the currentMinimumBound
	 * array. If this is the case, update the array to contain the smaller point.
	 * @param point The point containing X, Y and Z values of which the smallest ones should be extracted.
	 * @param currentMinimumBound The array containing the smallest X, Y and Z values (as indexes 0, 1 and 2 respectively.
	 */
	private void retainMinimumCoordinates(Point point, double[] currentMinimumBound) {
		currentMinimumBound[0] = Math.min(point.getX(), currentMinimumBound[0]);
		currentMinimumBound[1] = Math.min(point.getY(), currentMinimumBound[1]);
		currentMinimumBound[2] = Math.min(point.getZ(), currentMinimumBound[2]);
	}

	/**
	 * Checks if the X, Y and Z values in the specified point are larger then the ones found in the currentMaximumBound
	 * array. If this is the case, upat the array to contain the largest point.
	 * @param point The point containing X, Y and Z values of which the largest ones are should be extracted.
	 * @param currentMaximumBound The array containing the largest X, Y and Z values (as indexes 0, 1 and 2 respectively.
	 */
	private void retainMaximumCoordinates(Point point, double[] currentMaximumBound) {
		currentMaximumBound[0] = Math.max(point.getX(), currentMaximumBound[0]);
		currentMaximumBound[1] = Math.max(point.getY(), currentMaximumBound[1]);
		currentMaximumBound[2] = Math.max(point.getZ(), currentMaximumBound[2]);
	}
}
